
package com.ucitech.filedb;

import java.io.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 *
 * @author Charlee
 */
public abstract class PersistentMap<T extends Record>{
    private final FileDatabase fdb;
    private final Object lock = new Object();
    private final Map<String,T> m = new HashMap<String,T>();

    /**
     * Constructor parameters
     * @param fileName This is the filedb cache file
     * @param size This is the expected data size per record.
     * @throws IOException 
     */
    public PersistentMap(String fileName, int size) throws IOException {
        this.fdb = new FileDatabase(fileName, size);
    }
    
    /**
     * This method is used internally by the filedb framework
     * @param csvdata The comma separated record value
     * @return  Returns the record Object
     */
    public abstract T load(String csvdata);
    
    /**
     * retrieves a filedb Object from the file database
     * @param key The key associated with this database record
     * @return Returns the database record as an Object
     */
    public T get(String key){
        synchronized (lock) {
            return m.get(key);
        }
    }

    /**
     * Inserts a database record to the cache file
     * @param value The object value to be stored
     */
    public void insert(T value)  {
        synchronized (lock) {
            //Will need to remove spaces from String key
            if(fdb.writeln(value.key(), value.csv())){
                m.put(value.key(), value);
            }
        }
    }

    /**
     * Method maps to java.util.Collection
     * @param key 
     */
    public void remove(String key) {
        synchronized (lock) {
            if(fdb.writeln(key, "")){
                m.remove(key);
            }
        }
    }

    /**
     * Method maps to java.util.Collection
     * @return 
     */
    public Set<String> keySet() {
        synchronized (lock) {
            return m.keySet();
        }
    }
    
    /**
     * Method maps to java.util.Collection
     * @return 
     */
    public Collection<T> values() {
        synchronized (lock) {
            return m.values();
        }
    }
    
    /**
     * Method maps to java.util.Collection
     * @param key
     * @return 
     */
    public boolean containsKey(String key){
        synchronized (lock) {            
            return m.containsKey(key);
        }
    }
    
    /**
     * Method maps to java.util.Collection
     * @return 
     */
    public int size(){
        return m.size();
    }
    
    /**
     * Method maps to java.util.Collection.
     * This will also clear the records in the cache file
     */
    public void clear(){
        try {
            fdb.delete();
            m.clear();
        } catch (Exception e) {
            System.out.println();
        }
    }
    
    /**
     * Closes the file database instance
     */
    public void close(){
        try {
            fdb.close();
            m.clear();
        } catch (Exception ex) {
            System.out.println(ex);
        }
    }
    
    private class FileDatabase extends HashMap<String,Long>{
        private final RandomAccessFile rnd;
        private final int size;

        private FileDatabase(String fileName, int size) throws IOException {
            File file = file(fileName);
            this.rnd = new RandomAccessFile(file, "rwd");
            this.size = size;
            loadMapFile(file);
        }

        private T readln(String key) throws IOException {
            Long ptr = get(key);
            if (ptr != null) {
                rnd.seek(ptr);
                String str = rnd.readLine();
                String value = str.substring(str.indexOf(":")+1).trim();
                if (value.length() > 0) {
                    return load(value);
                }
            }   return null;
        }

        private boolean writeln(String key, String str) {
            try {
                Long ptr = get(key);
                if (ptr != null) {
                    rnd.seek(ptr);
                    rnd.write(pad(key, str));
                    rnd.write('\n');
                } else {
                    ptr = rnd.length();
                    rnd.seek(ptr);
                    rnd.write(pad(key, str));
                    rnd.write('\n');
                    put(key, ptr);
                }   return true;
            } catch (Exception ex) {
                System.out.println(ex);
                    return false;
            }
        }

        private void delete() throws IOException {
            rnd.setLength(0);
        }

        private void close() {
            try {
                rnd.close();
            } catch (Exception e) {
            }
        }
        
        private byte[] pad(String key, String value) {
            return pad(key.concat(":").concat(value));
        }

        private byte[] pad(String str) {
            StringBuilder sb = new StringBuilder(str);
            if (sb.length()>size) {
                throw new RuntimeException("Invalid input data length");
            } else {
                while (sb.length() < size) {
                    sb.append(" ");
                }
                return sb.toString().getBytes();
            }
            //return Arrays.copyOf(str.getBytes(), size);
        }
        
        private void loadMapFile(File file) throws IOException{
            BufferedReader br = null;
            String line; long i=0;
            try {
                br = new BufferedReader(new FileReader(file)); // new InputStreamReader(new FileInputStream(file), CHARSET)
                while((line = br.readLine())!=null){
                    if (line.length() == size) {
                        String key = line.substring(0, line.indexOf(":"));
                        String value = line.substring(line.indexOf(":")+1).trim();
                        put(key, (i*(size+1)));
                        if (value.length() > 0) {
                            m.put(key, load(value));
                        }
                    } else {
                        refactor(file);
                    }   i++;
                }
            } finally {
                close(br);
            }
        }

        private void refactor(File file) throws IOException {
            File temp = new File(file.getPath() + ".tmp");
            if(temp.isFile())temp.delete();

            BufferedReader br = null;
            FileOutputStream fw = null;
            try {
                br = new BufferedReader(new FileReader(file));
                fw = new FileOutputStream(temp, true);

                String line;
                while ((line = br.readLine()) != null) {
                    String s = line.trim();
                    if (s.indexOf(":") > 0) {
                        fw.write(pad(s));
                        fw.write('\n');
                    }
                }
                fw.flush();
            } finally {
                close(fw);
                close(br);
            }   throw new IOException("Invalid record file refactored");
        }
        
        private File file(String fileName) throws IOException {
            File ff = new File(fileName);
            if (!ff.isFile()) {
                String parent = ff.getParent();
                if(parent!=null)
                new File(parent).mkdirs();
                ff.createNewFile();
            }   return ff;
        }
        
        private void close(Closeable rd) {
            try {
                if(rd!=null)rd.close();
            } catch (Exception ex) {
            }
        }
    }
}
